using System;
using System.Collections.Generic;
using System.Text;

using System.Xml;
using System.Xml.Serialization;

namespace Horizon
{
    /// <summary>
    /// Methods for relating XML to objects
    /// </summary>
    class HorizonDataMapper
    {
        private Dictionary<string, string> dynamicFields;

        /// <summary>
        /// Update data regarding current items checked out for a given patron
        /// </summary>
        /// <param name="user">Patron whose checked out items should be updated</param>
        internal void UpdateItemsOut(Patron user, XmlDocument doc)
        {
            // Set Title to NULL to indicate which are not in list
            user.ItemsOut.ForEach(delegate(Holding _h)
            {
                _h.Title = null;
            });

            // Read checked-out books
            XmlNodeList itemsOut = doc.SelectNodes("//itemsoutdata/itemout");
            foreach (XmlNode node in itemsOut)
            {
                // Try to update existing holding if possible
                Holding h = user.ItemsOut.Find(
                    delegate(Holding _h)
                    {
                        return _h.BarCode == HorizonDataAccess.SafeInnerText(node.SelectSingleNode("holdingkey"));
                    });

                if (h == null) h = new Holding();

                PopulateStaticHoldingData(h, node);
                h.Patron = user;

                if (!user.ItemsOut.Contains(h))
                    user.ItemsOut.Add(h);
            }

            // Remove any holdings no longer in list
            user.ItemsOut.RemoveAll(
                delegate(Holding _h)
                {
                    return _h.Title == null;
                });
        }

        /// <summary>
        /// Reads common fields used to describe holdings.  Some messages
        /// move the fields around and must handle their exceptions.
        /// </summary>
        /// <param name="h">The holding whose data should be extracted</param>
        /// <param name="node">XML node containing holding data</param>
        internal void PopulateStaticHoldingData(Holding h, XmlNode node)
        {
            h.Title = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("TITLE/data/text"));
            h.Author = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("AUTHOR/data/text")); // Can be multiple entries...
            h.Publisher = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("PUBLISHER/data/text"));
            h.SmallImage = System.Uri.UnescapeDataString(HorizonDataAccess.SafeInnerText(node.SelectSingleNode("small_ec_image_url")));
            h.MediumImage = System.Uri.UnescapeDataString(HorizonDataAccess.SafeInnerText(node.SelectSingleNode("medium_ec_image_url")));
            h.LargeImage = System.Uri.UnescapeDataString(HorizonDataAccess.SafeInnerText(node.SelectSingleNode("large_ec_image_url")));

            h.Key = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("key")); // Unique ID for this holding?
            h.Link = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("TITLE/data/link/func")); // Direct web link to book (URL snippet)

            h.Location = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("location"));
            h.DueDate = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("duedate"));
            h.CheckOutDate = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("ckodate"));

            h.ISBN = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("isbn"));
            h.BarCode = HorizonDataAccess.SafeInnerText(node.SelectSingleNode("holdingkey"));
        }

        /// <summary>
        /// Maps holdings XML elements to Holding objects from search results
        /// </summary>
        /// <param name="doc">XML document containing search results</param>
        /// <returns>List of Holding objects</returns>
        internal List<Holding> GetHoldings(XmlDocument doc)
        {
            XmlNode node = doc.SelectSingleNode("//searchresults");

            List<Holding> holdings = new List<Holding>(50);

            // If results block isn't there, just return
            if (node == null) return holdings;

            XmlNodeList results = node.SelectNodes("results/row");

            // Read fields definitions from headings
            ReadDynamicFieldDefinitions(doc);

            // Read holdings information
            foreach (XmlNode result in results)
            {
                Holding h = new Holding();

                PopulateStaticHoldingData(h, result);

                h.Title = ReadDynamicField(result, "Title", h.Title);
                h.Author = ReadDynamicField(result, "Author", h.Author);
                h.PubDate = ReadDynamicField(result, "Pub Date", h.PubDate);
                h.Format = ReadDynamicField(result, "Format", h.Format);
                h.Publisher = ReadDynamicField(result, "Publisher", h.Publisher);
                //h.Type = ReadDynamicField(result, "Type", h.Type); // i.e. "In Library Use Only"
                h.Location = ReadDynamicField(result, "Location", h.Location);
                h.Collection = ReadDynamicField(result, "Collection", h.Collection);
                h.CallNumber = ReadDynamicField(result, "Call No.", h.CallNumber);
                h.Status = ReadDynamicField(result, "Status", h.Status);
                h.DueDate = ReadDynamicField(result, "Due Date", h.DueDate);

                holdings.Add(h);
            }
            return holdings;
        }

        /// <summary>
        /// Reads a named field from XML data, as defined in the header of the summary/searchresults/itemheader section
        /// </summary>
        /// <param name="node">XML node for this result which may or may not contain the value</param>
        /// <param name="field">The named field to return</param>
        /// <param name="defValue">Default value to return if no value was found</param>
        /// <returns>Found value or default</returns>
        internal string ReadDynamicField(XmlNode node, string field, string defValue)
        {
            if (dynamicFields.ContainsKey(field))
            {
                string val = HorizonDataAccess.SafeInnerText(node.SelectSingleNode(dynamicFields[field]));
                if (val == string.Empty) return defValue;
                else return val;
            }
            else return defValue;
        }

        /// <summary>
        /// Reads the itemheader block to determine where fields may exist in actual results.
        /// For example, in this example, it can be expected that Location and Collection can
        /// be extracted from the first and second columns respectively of the items/row section.
        /// Other values are stored in properly named tags.
        /// <itemheader>
        ///   <col>
        ///     <label>Location</label>
        ///   </col>
        ///   <col>
        ///     <label>Collection</label>
        ///   </col>
        /// </itemheader>
        /// </summary>
        /// <param name="doc"></param>
        internal void ReadDynamicFieldDefinitions(XmlDocument doc)
        {
            // Clear it out...
            dynamicFields = new Dictionary<string, string>();

            // Find header fields
            XmlNodeList labelNodes = doc.SelectNodes("//searchresults/header/col/label");

            for (int i = 0; i < labelNodes.Count; i++)
            {
                dynamicFields.Add(labelNodes[i].InnerText, "cell[" + (i + 1) + "]/data/text");
            }

            // Find item header fields
            labelNodes = doc.SelectNodes("//searchresults/itemheader/col/label");

            for (int i = 0; i < labelNodes.Count; i++)
            {
                dynamicFields.Add(labelNodes[i].InnerText, "//items/row/cell[" + (i + 1) + "]/data/text");
            }
        }

    }
}
